//
// VisibilityGraphGenerator.cs
// MSAGL base class to create the visibility graph for Rectilinear Edge Routing.
//
// Copyright Microsoft Corporation.

using System;
using System.Diagnostics;
using Microsoft.Msagl.Core.DataStructures;
using Microsoft.Msagl.Core.Geometry;
using Microsoft.Msagl.Core.Geometry.Curves;
using Microsoft.Msagl.Routing.Spline.ConeSpanner;
using Microsoft.Msagl.Routing.Visibility;

#if DEBUG
using System.Linq;
using Microsoft.Msagl.DebugHelpers;
#endif

namespace Microsoft.Msagl.Routing.Rectilinear {
    // Scan direction is parallel to the sweepline which moves in the perpendicular direction;
    // i.e. scan direction is "sideways" along the sweepline.  We also have lookahead scans
    // that enqueue events along the scan-primary coordinate (in the direction of the scan, i.e.
    // X for Hscan, Y for Vscan) to handle reflections from non-orthogonal obstacle sides, 
    // and lookback scans that have not had their reflections calculated because they reflect
    // backward from the scanline and thus must be picked up on a subsequent perpendicular sweep.
    internal abstract partial class VisibilityGraphGenerator {
        // The direction of the current scan - perpendicular to the sweep direction
        // (i.e. a vertical sweep will scan along a horizontal line).
        protected ScanDirection ScanDirection;

        /// <summary>
        /// The visibility graph generated by GenerateVisibilityGraph.
        /// </summary>
        internal VisibilityGraph VisibilityGraph { get; set; }

        protected VisibilityGraphGenerator(bool wantReflections) {
            this.ScanDirection = ScanDirection.HorizontalInstance;
            this.eventQueue = new EventQueue();
            this.HorizontalScanSegments = new ScanSegmentTree(ScanDirection.HorizontalInstance);
            this.VerticalScanSegments = new ScanSegmentTree(ScanDirection.VerticalInstance);
            this.wantReflections = wantReflections;
        }

        // The events we generate and process during the scan.
        protected EventQueue eventQueue;

        // The line segments produced during the vertex event processing.
        internal ScanSegmentTree HorizontalScanSegments;
        internal ScanSegmentTree VerticalScanSegments;

        protected ScanSegmentTree ParallelScanSegments        // Parallel to scanline
            { get { return ScanDirection.IsHorizontal ? HorizontalScanSegments : VerticalScanSegments; } }

        protected ScanSegmentTree PerpendicularScanSegments   // Perpendicular to scanline
            { get { return ScanDirection.IsHorizontal ? VerticalScanSegments : HorizontalScanSegments; } }

        // For lookahead points, we record the point of the intersection on the reflecting side, then
        // whenever we load a side, we check for active lookahead lines within this range.  Since we
        // are just intersecting rays, we only care about the X (H scan) or Y (V scan) coordinate.
        LookaheadScan lookaheadScan;
        private readonly bool wantReflections;

        // This is the tree of rectangle nodes of the input obstacles' padded encompassing polylines.
        internal ObstacleTree ObstacleTree = new ObstacleTree();

        // The scanline of obstacle sides, for processing events.
        protected RectilinearScanLine scanLine;

        // This ensures that sentinels lie outside the graph boundaries, to ensure the scanline orders
        // orders the sentinels properly.  Its borders are not drawn; instead the borders of objects
        // already contain padding and this is assumed to be sufficient to provide space for routing.
        internal const double SentinelOffset = 1.0;

        // This is a map of all group boundary crossings for the current scanline event being processed,
        // including the direction (along the scanline axis, horizontal or vertical) that a path crossing
        // that boundary takes toward the group's interior.  A point may be in here twice in the event of
        // groups sharing a boundary.
        protected readonly GroupBoundaryCrossingMap CurrentGroupBoundaryCrossingMap = new GroupBoundaryCrossingMap();

        // For scanline traversal.  
        protected readonly NeighborSides LowNeighborSides = new NeighborSides();
        protected readonly NeighborSides HighNeighborSides = new NeighborSides();

        internal static VisibilityGraph NewVisibilityGraph() {
            return new VisibilityGraph { VertexFactory = (point => new VisibilityVertexRectilinear(point)) };
        }

        /// <summary>
        /// Generate the visibility graph along which edges will be routed.
        /// </summary>
        /// <returns></returns>
        internal virtual void GenerateVisibilityGraph() {
            // Generate the Polyline tree from the padded shapes.  ObstacleTree allows us to do
            // obstacle hit-testing for dynamic obstacles, as well as providing input for the Nudger.
            // Each PolylinePoint contains a reference to the Polyline of which it is a member.
            if (null == ObstacleTree.Root) {
                return;
            }
            
            // Now enqueue initial events for the vertical sweep (horizontal scan); start with the lowest
            // vertices and then we'll load subsequent vertices (and intersections) as we encounter them.
            // We'll defer the generation of vertex events in the perpendicular direction until we're done
            // with this sweep, to save memory; but we may enqueue intersection events in the perpendicular
            // direction during this sweep.
            InitializeEventQueue(ScanDirection.HorizontalInstance);

            // For the first sweep, EnqueueBottomVertexEvents calculates our min/max boundaries.
            // Validate them here; we need to have enough room to create the sentinels.  We need
            // only the Padding of the adjacent obstacle(s) for correct line spacing outside these
            // extreme obstacles, so don't need to pad the sentinels.
            if ((ObstacleTree.GraphBox.Left <= (Double.MinValue + SentinelOffset))
                || (ObstacleTree.GraphBox.Bottom <= (Double.MinValue + SentinelOffset))
                || (ObstacleTree.GraphBox.Right >= (Double.MaxValue - SentinelOffset))
                || (ObstacleTree.GraphBox.Top >= (Double.MaxValue - SentinelOffset))
                ) {
                throw new InvalidOperationException("One or more obstacle boundaries are out of range");
            }

            // Create the sentinels and add them to the scanline.  Do NOT add them to the event queue
            // because we're not going to close them.  We're also effectively adding only the inner side
            // perpendicular to the scan, but we need the polyline for segment-subsumption tracking.
            // Creating with two points has the lines "clockwise" (and counterclockwise) as above.
            //
            // We don't need to store the sentinels as we retrieve them from [SweepEvent].Vertex.Polyline.
            // And we only need the low sentine's HighSide and the high sentinel's LowSide, but we use both
            // to save Obstacle from having to worry about the special case.  Note: reflection will
            // automatically not happen when hitting sentinels because their borders are IsPerpendicular.
            int scanlineSentinelOrdinal = Obstacle.FirstSentinelOrdinal;

            // Low sentinel...
            var lowerCorner = new Point(ObstacleTree.GraphBox.Left - SentinelOffset, ObstacleTree.GraphBox.Bottom - SentinelOffset);
            var upperCorner = new Point(ObstacleTree.GraphBox.Left - SentinelOffset, ObstacleTree.GraphBox.Top + SentinelOffset);
            var sentinel = Obstacle.CreateSentinel(lowerCorner, upperCorner, ScanDirection, scanlineSentinelOrdinal++);
            scanLine.Insert(sentinel.ActiveHighSide, ObstacleTree.GraphBox.LeftBottom);

            // High sentinel...
            lowerCorner = new Point(ObstacleTree.GraphBox.Right + SentinelOffset, ObstacleTree.GraphBox.Bottom - SentinelOffset);
            upperCorner = new Point(ObstacleTree.GraphBox.Right + SentinelOffset, ObstacleTree.GraphBox.Top + SentinelOffset);
            sentinel = Obstacle.CreateSentinel(lowerCorner, upperCorner, ScanDirection, scanlineSentinelOrdinal++);
            scanLine.Insert(sentinel.ActiveLowSide, ObstacleTree.GraphBox.LeftBottom);

            // Process the Hscan events.`
            DevTraceInfoVgGen(1, "Processing Horizontal Scan events");
            ProcessEvents();

            // Now do the horizontal sweep (vertical scan).  Note: Because we use Cartesian coordinates
            // rather than Page coordinates, the High and Low sides reverse the direction they traverse
            // when doing vertical scan; this information is passed as a ctor param.  This allows for
            // consistent comparisons.
            InitializeEventQueue(ScanDirection.VerticalInstance);

            // Lower sentinel...
            lowerCorner = new Point(ObstacleTree.GraphBox.Left - SentinelOffset, ObstacleTree.GraphBox.Bottom - SentinelOffset);
            upperCorner = new Point(ObstacleTree.GraphBox.Right + SentinelOffset, ObstacleTree.GraphBox.Bottom - SentinelOffset);
            sentinel = Obstacle.CreateSentinel(lowerCorner, upperCorner, ScanDirection, scanlineSentinelOrdinal++);
            scanLine.Insert(sentinel.ActiveHighSide, ObstacleTree.GraphBox.LeftBottom);

            // Upper sentinel
            lowerCorner = new Point(ObstacleTree.GraphBox.Left - SentinelOffset, ObstacleTree.GraphBox.Top + SentinelOffset);
            upperCorner = new Point(ObstacleTree.GraphBox.Right + SentinelOffset, ObstacleTree.GraphBox.Top + SentinelOffset);
            sentinel = Obstacle.CreateSentinel(lowerCorner, upperCorner, ScanDirection, scanlineSentinelOrdinal);
            scanLine.Insert(sentinel.ActiveLowSide, ObstacleTree.GraphBox.LeftBottom);

            // Process the Vscan events.
            DevTraceInfoVgGen(1, "Processing Vertical Scan events");
            ProcessEvents();
        }

        [Conditional("DEBUG")]
        // ReSharper disable InconsistentNaming
        protected static void Debug_AssertGraphIsRectilinear(VisibilityGraph graph, ObstacleTree obstacleTree)
        // ReSharper restore InconsistentNaming
        {
#if DEBUG
            if (graph.Edges.Any(edge => !PointComparer.IsPureDirection(PointComparer.GetDirections(edge.SourcePoint, edge.TargetPoint))))
            {
                StaticGraphUtility.Assert(false, "Generated VisibilityGraph contains non-rectilinear lines", obstacleTree, graph);
                return;
            }
#endif
        }

        internal static Point ScanLineIntersectSide(Point site, BasicObstacleSide side, ScanDirection scanDir)
        {
            // Note: we don't assert that site and side are not PointComparer.Equal, because ScanLine calls
            // this on sides that share vertices.
            Debug.Assert(!scanDir.IsFlat(side), "flat sides should not be in the scanline or encountered on lookahead scan");

            // We know that we will have an intersection if the side is adjacent in the scanline, so
            // we can optimize the calculation to project along the slope of the BasicObstacleSide.
            // Also, due to rounding, we need to make sure that when intersecting the side, we're not
            // falling short due to rounding error; that can be a problem if we're right at a vertex
            // of that obstacle, because then there is no intersection with the perpendicular line
            // from that vertex.  So make sure we are at least to the nearest coordinate of that side.

            // Note:  Calculate slope here using 'dir' rather than side.SlopeInverse because Reflection
            // lookaheads calculate the perpendicular intersection and side.Slope(Inverse) is always
            // relative to the scanline parallel.
            Point dir = side.Direction;
#if SHARPKIT //https://code.google.com/p/sharpkit/issues/detail?id=369
            Point intersect = side.Start.Clone();
#else
            Point intersect = side.Start;
#endif
            if (scanDir.IsHorizontal) {
                intersect.X += (dir.X / dir.Y) * (site.Y - side.Start.Y);
                intersect.X = SpliceUtility.MungeIntersect(site.X, intersect.X, side.Start.X, side.End.X);
                intersect.Y = site.Y;
            }
            else {
                intersect.X = site.X;
                intersect.Y += (dir.Y / dir.X) * (site.X - side.Start.X);
                intersect.Y = SpliceUtility.MungeIntersect(site.Y, intersect.Y, side.Start.Y, side.End.Y);
            }
            return intersect;
        }

        PolylinePoint GetOpenVertex(Polyline poly) {
            PolylinePoint lowest = poly.StartPoint;
            PolylinePoint next = TraversePolylineForEvents(lowest);

            // We want the bottom vertex to be the lowest in scanline-parallel coordinate if there is a
            // flat bottom side, and we've guaranteed that the lines are oriented clockwise.  This means
            // that we want a <= comparison of the current node vs. the candidate, to store the last
            // lowest vertex of the clockwise rotation.  Stop when we've turned upward from descending/flat.
            int iPrevCmp = PointCompare(next.Point, lowest.Point);
            for (; ; next = TraversePolylineForEvents(next)) {
                int iCurCmp = PointCompare(next.Point, lowest.Point);
                if (iCurCmp <= 0) {
                    lowest = next;
                } else if ((iCurCmp > 0) && (iPrevCmp <= 0)) {
                    break;
                }
                iPrevCmp = iCurCmp;
            }
            return lowest;
        }

        PolylinePoint TraversePolylineForEvents(PolylinePoint polyPoint) {
            // When loading scanline events, we'll go clockwise for horizontal scan, where the
            // scanline-parallel coordinate increases to the right, or counterclockwise for vertical
            // scan, where the scanline-parallel coordinate increases to the left.
            if (ScanDirection.IsHorizontal) {
                return polyPoint.NextOnPolyline;
            }
            return polyPoint.PrevOnPolyline;
        }

        internal virtual void InitializeEventQueue(ScanDirection scanDir) {
            ScanDirection = scanDir;
            eventQueue.Reset(ScanDirection);
            EnqueueBottomVertexEvents();
            scanLine = new RectilinearScanLine(ScanDirection, ObstacleTree.GraphBox.LeftBottom);
            lookaheadScan = new LookaheadScan(ScanDirection);
        }

        void EnqueueBottomVertexEvents() {
            foreach (var obstacle in ObstacleTree.GetAllPrimaryObstacles()) {
                PolylinePoint bottomVertex = GetOpenVertex(obstacle.VisibilityPolyline);
                eventQueue.Enqueue(new OpenVertexEvent(obstacle, bottomVertex));
            }
        } // end EnqueueBottomVertexEvents

        internal bool IsFlat(BasicObstacleSide side) {
            return ScanDirection.IsFlat(side);
        }
        internal bool IsPerpendicular(BasicObstacleSide side) {
            // If it's perpendicular we won't generate reflections.
            return ScanDirection.IsPerpendicular(side);
        }

        // Params are event site (vertex point) and the obstacle side adjacent to that site.
        protected Point ScanLineIntersectSide(Point site, BasicObstacleSide side) {
            return ScanLineIntersectSide(site, side, ScanDirection);
        }

        protected bool SideReflectsUpward(BasicObstacleSide side) {
            // Returns false if vertical.
            if (side is LowObstacleSide) {
                // Low side slopes upward if slope is positive (to the high direction).
                return ScanDirection.Coord(side.End) > ScanDirection.Coord(side.Start);
            }
            // High side slopes upward if slope is negative (to the low direction).
            return ScanDirection.Coord(side.End) < ScanDirection.Coord(side.Start);
        }

        bool SideReflectsDownward(BasicObstacleSide side) {
            // Returns false if vertical.
            if (side is LowObstacleSide) {
                // Low side slopes downward if slope is negative (to the low direction).
                return ScanDirection.Coord(side.End) < ScanDirection.Coord(side.Start);
            }
            // High side slopes downward if slope is positive (to the high direction).
            return ScanDirection.Coord(side.End) > ScanDirection.Coord(side.Start);
        }

        // Calculate reflections from the lines, depending on line side (Low vs. High) and slope.
        // Because the low neighbor intersection is on a high side of its obstacle
        // and vice-versa, then the "side" of a lowNbor is a highSide, and vice versa.
        protected void StoreLookaheadSite(Obstacle initialObstacle, BasicObstacleSide reflectingSide, Point reflectionSite, bool wantExtreme) {
            if (!this.wantReflections) {
                return;
            }

            // If the line is perpendicular, we won't generate reflections (they'd be redundant).
            if (!IsPerpendicular(reflectingSide)) {
                // If this is hitting an extreme vertex in the forward direction, we will (or already did) create a normal
                // ScanSegment in the perpendicular direction.
                if (!wantExtreme && !StaticGraphUtility.PointIsInRectangleInterior(reflectionSite, reflectingSide.Obstacle.VisibilityBoundingBox)) {
                    return;
                }

                // We can only do upward reflections, which fortunately is all we need.
                if (SideReflectsUpward(reflectingSide)) {
                    // We defer actually creating the perpendicular line until we've processed
                    // the reflection event we're about to enqueue, so we may legitimately encounter
                    // two (or more) reflections at the same point along the parallel-to-scanline
                    // coordinate, if we have a side that is nearly but not quite perpendicular to
                    // the scanline.  For example:
                    //      \
                    //       \----------------------------- line2
                    //        \---------------------------- line1
                    // Assume the vertical side is very close to perpendicular; then as we process
                    // the horizontal lines in the upward direction, we may generate two lookahead
                    // reflections at coordinates that are sufficiently far apart in the vertical
                    // coordinate (in this example, Y) that the lines are not subsumed, but are
                    // sufficiently close in the horizontal coordinate (in this example, X) that
                    // the perpendicular lines would be subsumed. In that case, we look here to see
                    // if there is an active lookahead scan for the reflecting site's parallel coordinate;
                    // if so, then because we know that the side reflects upward, we also know that
                    // the perpendicular line from the lowest segment's reflection site will intersect
                    // the higher segments here, providing the VisibilityVertices we need, so we can
                    // safely ignore the duplicate lookahead.

                    // Don't worry about enqueueing a reflection at the extreme scanline-parallel
                    // vertex because we'll MergeSegments to handle that.
                    if (null == lookaheadScan.Find(reflectionSite)) {
                        lookaheadScan.Add(new BasicReflectionEvent(initialObstacle, reflectingSide.Obstacle, reflectionSite));
                        DevTraceInfoVgGen(1, "Storing reflection lookahead site {0}", reflectionSite);
                    }
                    else {
                        DevTraceInfoVgGen(1, "Reflection lookahead site {0} already exists", reflectionSite);
                    }
                }
            }
        } // end StoreLookaheadSite()

        // Load any lookahead scan ray intersections with a side we've just added.
        protected void LoadReflectionEvents(BasicObstacleSide sideToQueue) {
            LoadReflectionEvents(sideToQueue, sideToQueue);
        }

        // sideWithRange is either the same as sideToQueue, if that side is being loaded by an
        // OpenVertexEvent, or is a different side that is just closing.
        protected void LoadReflectionEvents(BasicObstacleSide sideToQueue, BasicObstacleSide sideWithRange) {
            // If this line reflects upward then it cannot receive rays from below (they would pass
            // through its obstacle), and of course a perpendicular lookahead line will never
            // intersect a perpendicular side.
            if ((null == sideToQueue) || SideReflectsUpward(sideToQueue) || IsPerpendicular(sideToQueue)) {
                return;
            }

            // If there is no overlap in the rectangles along the current axis, there is nothing
            // to do.  This reduces (but doesn't prevent) duplicate events being loaded.
            var bbox1 = new Rectangle(sideToQueue.Start, sideToQueue.End);
            var bbox2 = new Rectangle(sideWithRange.Start, sideWithRange.End);
            if ((ScanDirection.IsHorizontal) ? !bbox1.IntersectsOnX(bbox2) : !bbox1.IntersectsOnY(bbox2)) {
                return;
            }

            // Make sure we order the endpoints from low to high along the scanline parallel, and get only
            // the intersection.  RectilinearFileTests.Nudger_Overlap* exercise reflection lookahead subranges.
            Rectangle bboxIntersect = Rectangle.Intersect(bbox1, bbox2);
            Point low = bboxIntersect.LeftBottom;
            Point high = bboxIntersect.RightTop;

            // This is inclusive of the endpoints of sideWithRange, to be sure that we remove the item
            // from LookaheadScan; if it's on an extreme vertex in the perpendicular sweep then it will
            // stop the chain; see TestRectilinear.Reflection_Staircase_Stops_At_BoundingBox_Side*.
            RBNode<BasicReflectionEvent> lookaheadSiteNode = lookaheadScan.FindFirstInRange(low, high);
            while (null != lookaheadSiteNode) {
                // Calculate the lookahead intersection with this side in the perpendicular direction to
                // the scanline.  Note: due to rounding error, this may be different from the calculation
                // in the parallel direction when the scanline gets up to the ScanDirection.PerpCoord(intersect);
                // this will be adjusted in ScanSegmentTree.MergeSegments.
                Point intersect = ScanLineIntersectSide(lookaheadSiteNode.Item.Site, sideToQueue
                            , ScanDirection.PerpendicularInstance);
                DevTraceInfoVgGen(1, "Loading reflection from lookahead site {0} to intersect at {1}"
                            , lookaheadSiteNode.Item.Site, intersect);
                DevTraceInfoVgGen(2, "     side {0})", sideToQueue);    // same indent as AddSegment

                // In some cases where the ActiveLowSide and ActiveHighSide of an obstacle lean in the same
                // direction such that LowSide is above HighSide, e.g. on the horizontal pass when they both
                // lean to the right, the delayed-lookahead in CloseVertex (obstacle close) event may find the
                // high side spanning the scanline-parallel coordinate range where its low side has enqueued
                // lookahead events.  In that case the intersection will be less than the enqueueing site so
                // ignore it.  See RectilinearTests.ReflectionsSitedByLowSideAreNotLoadedByHighSide.)
                // Similarly, if this is at the same perpendicular coordinate as the current scanline
                // position, ignore it; otherwise we could back up in the scanline's parallel coordinate.
                // Since we retrieved events for the endpoint of any previous side, this won't be
                // encountered on a bend vertex event; therefore we're on a near-flat bottom side
                // so we're parallel to the extreme-vertex line and it's fine to just absorb the photon.
                // This also handles the case of reflections into intersecting sides - at some point
                // they converge such that the intersection is not ahead of the lookahead site.
                if (ScanDirection.ComparePerpCoord(intersect, lookaheadSiteNode.Item.Site) > 0) {
                    // Add an event to continue the chain, "shifting" the site's reflecting
                    // obstacle back to the initialObstacle position.  We must load this here 
                    // and process it in ConfirmLookaheadEvent so it will be removed from
                    // the lookahead list; we can't remove it here if it doesn't satisfy the
                    // staircase requirements, because this may be called from loading a "higher"
                    // side (during the sweep) which could steal events from the lower side.
                    AddReflectionEvent(lookaheadSiteNode.Item, sideToQueue, intersect);
                }
                else {
                    if (lookaheadSiteNode.Item.ReflectingObstacle != sideToQueue.Obstacle) {
                        DevTraceInfoVgGen(1, "  (discarding reflection at intersect {0} as it is not ahead of the previous site)", intersect);

                        // We need to remove the site.  We're in the middle of Node enumeration so just
                        // mark the site and on function exit we'll remove any so marked.
                        lookaheadScan.MarkStaleSite(lookaheadSiteNode.Item);
                    }
                    else {
                        DevTraceInfoVgGen(1, "  (skipping reflection at intersect {0} as it is the same obstacle)", intersect);
                    }
                }

                // Get the next item, leaving the current one in the lookahead scan until
                // we actually process the event; this lets us know whether an intervening
                // obstacle may be opened and intercepted the reflection. ConfirmLookaheadEvents
                // will actually do the removal when the lowest side containing the lookahead
                // site is loaded.  See RectilinearTests.ReflectionsRemoveInterceptedSite.
                lookaheadSiteNode = lookaheadScan.FindNextInRange(lookaheadSiteNode, high);
            } // endwhile previousSiteNode

            lookaheadScan.RemoveStaleSites();
        }

        // Determine whether the event is valid and do some common processing.
        bool AddPerpendicularReflectionSegment(BasicReflectionEvent currentEvent, BasicObstacleSide eventSide, BasicObstacleSide nborSide) {
            // If eventSide is null it means we had the wrong side type as a scanline neighbor.
            // If another obstacle opened up, then that obstacle (or another intervening one) should have
            // drained this reflection event.
            Debug.Assert(null != eventSide, "eventSide should not be null");

            // Between the time currentEvent was queued and the time we're now processing it, another
            // obstacle may have opened between the previousSite and the eventSite, in which case it
            // removed currentEvent from the queue already.  So currentEvent may be stale.  The new
            // obstacle may have stored *another* lookahead site with the same scanline-parallel 
            // coordinate (but higher up perpendicularly).  So remove the exact site of currentEvent; 
            // otherwise the currentEvent could be a stale event with the lower scanline-parallel
            // coordinate, and would remove the site from the lookahead list before the "live" event
            // looks for it.  See RectilinearTests.ReflectionsRemoveInterceptedSite.
            if (lookaheadScan.RemoveExact(currentEvent.PreviousSite)) {
                Debug.Assert(currentEvent.InitialObstacle == currentEvent.PreviousSite.ReflectingObstacle
                            , "Inconsistency: currentEvent.InitialObstacle != currentEvent.PreviousSite.ReflectingObstacle");
// ReSharper disable HeuristicUnreachableCode
// ReSharper disable ConditionIsAlwaysTrueOrFalse
                if (null == eventSide) {
                    // We've removed the event so there's nothing else to do.
                    return false;
                }
// ReSharper restore ConditionIsAlwaysTrueOrFalse
// ReSharper restore HeuristicUnreachableCode

                // If the two sides intersect ahead of the scanline, we don't want the reflection.

                // If the reflecting side is flat, no reflection is done - that's handled by OpenVertexEvent.
                Debug.Assert(!IsFlat(eventSide), "Flat sides should not be encountered in reflections");
                if (currentEvent.PreviousSite.IsStaircaseStep(currentEvent.ReflectingObstacle)) {
                    // We need to draw the perpendicular lines here because we may be on the second
                    // sweep so there won't be a subsequent sweep to draw them.  And if we're on the
                    // second sweep, we may have already loaded this segment as part of a continuation
                    // of an overlapped segment. Either way, we only want this if we are not on an extreme
                    // edge of the target obstacle (reflectingObstacle for the perpendicular segment,
                    // nborSide.Obstacle for the parallel segment).  Extreme vertices will generate segments.
                    // See TestRectilinear.Reflection_Staircase_Stops_At_BoundingBox_Side*.
                    if (!StaticGraphUtility.PointIsInRectangleInterior(currentEvent.Site, currentEvent.ReflectingObstacle.VisibilityBoundingBox))
                    {
                        return false;
                    }
                    DevTraceInfoVgGen(1, "Perpendicular Reflection - Adding Segment [{0} -> {1}]", currentEvent.PreviousSite.Site, currentEvent.Site);
                    DevTraceInfoVgGen(2, "  -> side {0}", eventSide);   // same indent as AddSegment; eventSide is highNbor
                    if (!InsertPerpendicularReflectionSegment(currentEvent.PreviousSite.Site, currentEvent.Site)) {
                        return false;
                    }

                    // If the neighbor continues the staircase and the parallel segment would hit a non-extreme point
                    // on the neighbor, return true and the Low/HighReflectionEvent handler will add the parallel segment.
                    if ((null != nborSide) && currentEvent.IsStaircaseStep(nborSide.Obstacle)) {
                        return this.ScanLineCrossesObstacle(currentEvent.Site, nborSide.Obstacle);
                    }
                    DevTraceInfoVgGen(1, "Reflection Lookahead site {0} is not an outgoing staircase step; discontinuing", currentEvent.PreviousSite);
                }
                else {
                    DevTraceInfoVgGen(1, "Reflection Lookahead site {0} is not an incoming staircase step; discontinuing", currentEvent.PreviousSite);
                }
            }
            else {
                DevTraceInfoVgGen(1, "Reflection Lookahead site {0} is no longer in the lookahead table; skipping", currentEvent.PreviousSite);
            }
            return false;
        }

        protected abstract bool InsertPerpendicularReflectionSegment(Point start, Point end);

        private bool AddParallelReflectionSegment(Obstacle eventObstacle, BasicObstacleSide lowNborSide
                                , BasicObstacleSide highNborSide, BasicReflectionEvent action) {
            // If this is reflecting to a low neighbor, then that intersect is 'start' in the low-to-high
            // sequence, and the event site is the end; otherwise we start at the event site and end at
            // the high neighbor.
            Point intersect = ScanLineIntersectSide(action.Site, lowNborSide ?? highNborSide);
            Point start = (null != lowNborSide) ? intersect : action.Site;
            Point end = (null != lowNborSide) ? action.Site : intersect;
            
            // Now get the opposite neighbors so AddSegment can continue the reflection chain.
            if (null == lowNborSide) {
                lowNborSide = scanLine.NextLow(highNborSide).Item;
            }
            else {
                highNborSide = scanLine.NextHigh(lowNborSide).Item;
            }
            return InsertParallelReflectionSegment(start, end, eventObstacle, lowNborSide, highNborSide, action);
        }

        protected abstract bool InsertParallelReflectionSegment(Point start, Point end, Obstacle eventObstacle,
                BasicObstacleSide lowNborSide, BasicObstacleSide highNborSide, BasicReflectionEvent action);

        void AddReflectionEvent(BasicReflectionEvent previousSite, BasicObstacleSide side, Point site) {
            Debug.Assert(null != scanLine.Find(side), "AddReflectionEvent could not find 'side' in the scanline");

            // Add an event that will be drained when a side spanning the scanline-parallel is loaded
            // as the sweep moves "up".
            var lowSide = side as LowObstacleSide;
            if (lowSide != null) {
                eventQueue.Enqueue(new LowReflectionEvent(previousSite, lowSide, site));
            } else {
                eventQueue.Enqueue(new HighReflectionEvent(previousSite, (HighObstacleSide)side, site));
            }
        }

        RBNode<BasicObstacleSide> AddSideToScanLine(BasicObstacleSide side, Point scanPos) {
            RBNode<BasicObstacleSide> node = scanLine.Insert(side, scanPos);

            // Now get any pending LookaheadScan intersections along this side.
            LoadReflectionEvents(side);
            return node;
        }

        void RemoveSideFromScanLine(RBNode<BasicObstacleSide> sideNode, Point scanPos) {
            scanLine.Remove(sideNode.Item, scanPos);
        }

        int PointCompare(Point lhs, Point rhs) {
            return ScanDirection.Compare(lhs, rhs);
        }

        internal virtual void Clear() {
            ObstacleTree.Clear();
            eventQueue = new EventQueue();
            HorizontalScanSegments = new ScanSegmentTree(ScanDirection.HorizontalInstance);
            VerticalScanSegments = new ScanSegmentTree(ScanDirection.VerticalInstance);
            VisibilityGraph = null;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
        private void ProcessEvents() {
            // Note: Sentinel vertices are not in EventQueue so eventcount will go to 0.
            while (eventQueue.Count > 0) {
                SweepEvent evt = eventQueue.Dequeue();
                if (evt is OpenVertexEvent) {
                    ProcessEvent(evt as OpenVertexEvent);
                } else if (evt is LowBendVertexEvent) {
                    ProcessEvent(evt as LowBendVertexEvent);
                } else if (evt is HighBendVertexEvent) {
                    ProcessEvent(evt as HighBendVertexEvent);
                } else if (evt is CloseVertexEvent) {
                    ProcessEvent(evt as CloseVertexEvent);
                } else if (evt is LowReflectionEvent) {
                    ProcessEvent(evt as LowReflectionEvent);
                } else if (evt is HighReflectionEvent) {
                    ProcessEvent(evt as HighReflectionEvent);
                } else {
                    ProcessCustomEvent(evt);
                }

#if DEVTRACE
                if (evt is BasicVertexEvent) {
                    scanLine.DevTrace_VerifyConsistency("after {0}", evt);
                }
#endif // DEVTRACE
                LowNeighborSides.Clear();
                HighNeighborSides.Clear();
            } // endwhile there are events

            // Ensure we have no leftovers in the scanline - we should have the two sentinels and nothing else.
            Debug.Assert(2 == scanLine.Count, "There are leftovers in the scanline");
        }

        protected virtual void ProcessCustomEvent(SweepEvent evt)
        {
            // These are events specific to the derived class; by default there are none.
            Debug.Assert(false, "Unknown event type");
        }

        protected bool ScanLineCrossesObstacle(Point eventSite, Obstacle obstacle) {
            // An inner or outer neighbor's side is only an overlap start/stop candidate if its obstacle 
            // brackets the open/close event's Perpendicular Scan coord.
            return (ScanDirection.ComparePerpCoord(eventSite, obstacle.VisibilityBoundingBox.LeftBottom) > 0)
                && (ScanDirection.ComparePerpCoord(eventSite, obstacle.VisibilityBoundingBox.RightTop) < 0);
        }

        void FindInitialNeighborSides(RBNode<BasicObstacleSide> sideNode, out RBNode<BasicObstacleSide> lowNborSideNode,
                                      out RBNode<BasicObstacleSide> highNborSideNode)
        {
            lowNborSideNode = this.scanLine.NextLow(sideNode);
            highNborSideNode = this.scanLine.NextHigh(sideNode);
        }

        // As described in the doc, we stop at the first neighbor of the appropriate side type that we touch 
        // the border of, even if that's just skimming along the extreme vertex of it, because those will
        // continue the chain of open/close+addSegment, and we don't want to follow the full length of the
        // segment each time if there are a lot of collinear obstacle open/close events.
        protected void FindNeighbors(BasicVertexEvent vertexEvent, RBNode<BasicObstacleSide> lowSideNode, RBNode<BasicObstacleSide> highSideNode) {
            LowNeighborSides.Clear();
            HighNeighborSides.Clear();

            // Find the first HighObstacleSide in the low (scanline-decreasing) direction (this may be the low
            // sentinel) and the lowest LowObstacleSide toward that that we cross *through*, if any.  Then do
            // the same thing in the high direction.  If we are not overlapped, then we'll jump out immediately
            // from SkipToNeighbor, so there won't be a lot of redundant effort in that case.
            FindNeighbors(vertexEvent, lowSideNode, LowNeighborSides);
            FindNeighbors(vertexEvent, highSideNode, HighNeighborSides);
        }

        protected void FindNeighbors(BasicVertexEvent vertexEvent, RBNode<BasicObstacleSide> sideNode, NeighborSides neighborSides) {
            // vertexEvent.Site is on one of vertexEvent.Obstacle.Active(Low|High)Side, so we must get the 
            // appropriate vertex on whichever one of those Active*Sides is sideNode.
            var sideReferencePoint = (vertexEvent is OpenVertexEvent) ? sideNode.Item.Start : sideNode.Item.End;
            RBNode<BasicObstacleSide> initialLowNbor, initialHighNbor;
            this.FindInitialNeighborSides(sideNode, out initialLowNbor, out initialHighNbor);
            this.SkipToNeighbor(this.ScanDirection.OppositeDirection, sideNode.Item, sideReferencePoint, initialLowNbor, neighborSides);
            this.SkipToNeighbor(this.ScanDirection.Direction, sideNode.Item, sideReferencePoint, initialHighNbor, neighborSides);
        }

        void SkipToNeighbor(Directions nborSearchDir, BasicObstacleSide side, Point sideReferencePoint,
                            RBNode<BasicObstacleSide> nborNode, NeighborSides neighborSides) {
            // Find the first neighbor side (LowObstacleSide if going high, HighObstacleSide if going low) and
            // the side of opposite type (which would potentially end overlap), that that we cross *through*, if any.
            RBNode<BasicObstacleSide> overlapSideNode = null;
            BasicObstacleSide interveningGroupSide = null;
            for (; ; nborNode = scanLine.Next(nborSearchDir, nborNode)) {
                // Ignore the opposite side of the current obstacle.
                if (nborNode.Item.Obstacle == side.Obstacle) {
                    continue;
                }

                if (nborNode.Item.Obstacle.IsGroup) {
                    if (ProcessGroupSideEncounteredOnTraversalToNeighbor(nborNode, sideReferencePoint, nborSearchDir)) {
                        // Keep the first one (outermost) encountered.
                        if (null == interveningGroupSide) {
                            interveningGroupSide = nborNode.Item;
                        }
                    }
                    continue;
                }

                // Check for overlap-ending obstacle.
                if ((nborNode.Item is HighObstacleSide) == StaticGraphUtility.IsAscending(nborSearchDir)) {
                    if (ScanLineCrossesObstacle(sideReferencePoint, nborNode.Item.Obstacle)) {
                        overlapSideNode = nborNode;
                        interveningGroupSide = null;
                    }
                    continue;
                }

                // If we're here, we found the neighbor we were looking for.
                break;
            }

            neighborSides.SetSides(nborSearchDir, nborNode, overlapSideNode, interveningGroupSide);
        }

        private bool ProcessGroupSideEncounteredOnTraversalToNeighbor(RBNode<BasicObstacleSide> nborNode, Point sideReferencePoint,
                                                                                Directions nborSearchDir) {
            if (!this.ScanLineCrossesObstacle(sideReferencePoint, nborNode.Item.Obstacle)) {
                return false;
            }

            // We don't stop overlap or neighbor-traversal for groups, because we must go through the boundary;
            // neither do we create overlapped edges (unless we're inside a non-group obstacle).  Instead we turn
            // the boundary crossing on or off based on group membership at ShortestPath-time.
            Directions dirToInsideOfGroup = ((nborNode.Item is LowObstacleSide) == StaticGraphUtility.IsAscending(nborSearchDir))
                    ? nborSearchDir : CompassVector.OppositeDir(nborSearchDir);
            var intersect = this.ScanLineIntersectSide(sideReferencePoint, nborNode.Item);
            this.CurrentGroupBoundaryCrossingMap.AddIntersection(intersect, nborNode.Item.Obstacle, dirToInsideOfGroup);
            return true;
        }

        void FindNeighborsAndProcessVertexEvent(RBNode<BasicObstacleSide> lowSideNode
                                                , RBNode<BasicObstacleSide> highSideNode
                                                , BasicVertexEvent vertexEvent) {
            CurrentGroupBoundaryCrossingMap.Clear();
            FindNeighbors(vertexEvent, lowSideNode, highSideNode);
            this.ProcessVertexEvent(lowSideNode, highSideNode, vertexEvent);

            // Clear this again because we don't want Reflections to access stale values.
            CurrentGroupBoundaryCrossingMap.Clear();
        }

        protected abstract void ProcessVertexEvent(RBNode<BasicObstacleSide> lowSideNode,
                    RBNode<BasicObstacleSide> highSideNode, BasicVertexEvent vertexEvent);

        [Conditional("DEVTRACE")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        // ReSharper disable InconsistentNaming
        protected void DevTrace_DumpScanSegmentsDuringAdd(int verboseLevel) {
            // ReSharper restore InconsistentNaming
#if DEVTRACE
            DevTraceScanSegmentDump(verboseLevel);
#endif // DEVTRACE
        }

        void ProcessEvent(OpenVertexEvent openVertEvent) {
            // First insert the two new lines into the scanline.  Note: Although the lines are clockwise oriented,
            // LowObstacleSide and HighObstacleSide take a parameter to know when to go counterclockwise.
            var obstacle = openVertEvent.Obstacle;
            obstacle.CreateInitialSides(openVertEvent.Vertex, ScanDirection);
            Debug.Assert(!IsFlat(obstacle.ActiveLowSide), "OpenVertexEvent ActiveLowSide should not be flat");
            Debug.Assert(!IsFlat(obstacle.ActiveHighSide), "RemoveCollinearSides should have been called");
            DevTraceIfFlatSide(true /*isObstacleOpen*/, obstacle.ActiveLowSide.Start, obstacle.ActiveHighSide.Start);

            // Adding can rotate the RBTree which modifies RBNodes so get the lowSideNode after adding highSideNode.
            // AddSideToScanLine loads any reflection events for the side.
            AddSideToScanLine(obstacle.ActiveLowSide, openVertEvent.Site);
            RBNode<BasicObstacleSide> highSideNode = AddSideToScanLine(obstacle.ActiveHighSide, openVertEvent.Site);
            RBNode<BasicObstacleSide> lowSideNode = scanLine.Find(obstacle.ActiveLowSide);

            // Get the neighbors.  In the simple, non-overlapped case, we'll generate a segment between them which
            // includes the vertex point (and any flat border of the current obstacle).  These neighbors will
            // never be null; one or both may be the fake sentinel borders at the graphBox limits.
            this.FindNeighborsAndProcessVertexEvent(lowSideNode, highSideNode, openVertEvent);

            // Look for Reflections.  If the ScanSegments we just added generated any
            // ReflectionEvents, then the Active*Side to that side may cover them, or if we're overlapped,
            // the Active*Side of the overlapping obstacle may if there is a non-overlapped segment extension.

            // Check the neighbors in both directions.
            var lowReflector = LowNeighborSides.GroupSideInterveningBeforeLowNeighbor ?? LowNeighborSides.LowNeighborSide;
            if (SideReflectsUpward(lowReflector)) {
                LoadReflectionEvents(obstacle.ActiveLowSide);
            }
            var highReflector = HighNeighborSides.GroupSideInterveningBeforeHighNeighbor ?? HighNeighborSides.HighNeighborSide;
            if (SideReflectsUpward(highReflector)) {
                LoadReflectionEvents(obstacle.ActiveHighSide);
            }

            // If this is a flat side it must absorb any outstanding reflection sites.
            if (obstacle.ActiveHighSide.Start != obstacle.ActiveLowSide.Start) {
                // Create a temp HighObstacleSide so the "next vertex" moves in the correct direction.
                var tempSide = new HighObstacleSide(obstacle, openVertEvent.Vertex, ScanDirection);
                lookaheadScan.RemoveSitesForFlatBottom(tempSide.Start, tempSide.End);
            }

            // Add events for the low and high sides.
            EnqueueLowBendVertexEvent(obstacle.ActiveLowSide);
            EnqueueHighBendOrCloseVertexEvent(obstacle.ActiveHighSide);
        } // end ProcessEvent(OpenVertexEvent)

        void ProcessEvent(LowBendVertexEvent lowVertEvent) {
            // Note:  we only draw lines only from "interesting" vertices, which would be those
            // that open or close an obstacle, as well as staircase Reflections (see doc). This means
            // Low/HighVertexEvents routines will just track the change in ActiveLowSide/ActiveHighSide.

            // This is a vertex on the low side of the obstacle.  Update the ActiveLowSide in the obstacle
            // and scanline (this also checks for Reflection events).
            var obstacle = lowVertEvent.Obstacle;
            var lowSide = new LowObstacleSide(obstacle, lowVertEvent.Vertex, ScanDirection);

            // If the new lowSide is flat we don't remove it due to potential overlaps; that lets any collinear 
            // OpenVertexEvents know they are overlapped (touching == overlap).  When we get to CloseVertexEvent,
            // keeping the current ActiveLowSide in the scanline tells us how when the interior sides stop
            // so we know when we've found a neighbor.  Similarly, if we're turning down toward the
            // scanline, we let CloseVertexEvent remove the side (in case there are coincident vertices).
            // That leaves the case of still ascending, where we replace the side in the scanline.
            if (ScanDirection.ComparePerpCoord(lowSide.End, lowSide.Start) > 0) {
                this.RemoveSideFromScanLine(this.scanLine.Find(obstacle.ActiveLowSide), lowVertEvent.Site);
                AddSideToScanLine(lowSide, lowVertEvent.Site);
                obstacle.ActiveLowSide = lowSide;
                EnqueueLowBendVertexEvent(lowSide);
            }
        } // end ProcessEvent(LowBendVertexEvent)

        void EnqueueLowBendVertexEvent(BasicObstacleSide lowSide) {
            // We've already ensured the extension is valid so just queue the next event.
            eventQueue.Enqueue(new LowBendVertexEvent(lowSide.Obstacle, lowSide.EndVertex));
        }

        void ProcessEvent(HighBendVertexEvent highVertEvent) {
            // See comments in LowBendVertexEvent; this is mostly the same thing to the other side.
            var obstacle = highVertEvent.Obstacle;
            var highSide = new HighObstacleSide(obstacle, highVertEvent.Vertex, ScanDirection);

            this.RemoveSideFromScanLine(this.scanLine.Find(obstacle.ActiveHighSide), highVertEvent.Site);
            RBNode<BasicObstacleSide> highSideNode = AddSideToScanLine(highSide, highVertEvent.Site);
            obstacle.ActiveHighSide = highSide;
            EnqueueHighBendOrCloseVertexEvent(obstacle.ActiveHighSide);

            // If this is an extreme high-side lateral vertex on the horizontal pass turning to an upward-reflecting
            // side - i.e. if it is a vertex on the right-most border of the bounding box, and the new HighObstacleSide
            // has negative slope - then we may have a situation where a neighbor LowObstacleSide reflects downward and
            // spans this entire HighObstacleSide and a little more:
            //     #
            //       #
            //      .  #
            //       \   #
            //        .    #
            //               #
            // The ".\." side is completely spanned by the '#' side and because of the tilt directions, lookahead
            // will not happen, therefore reflections will not happen and there will be no scansegments between the
            // two sides.  This could lead to spurious overlaps.  So in this case we drop in an extra lookahead.
            // (This is not an issue for other tilt directions - there is always a reflection chain generated).
            // Test is RectilinearFileTests.Overlap_ExtremeSide_Lookahead.
            if (this.wantReflections && this.ScanDirection.IsHorizontal 
                    && (highSide.Start.X == obstacle.VisibilityBoundingBox.Right)
                    && SideReflectsUpward(highSide)) {
                var nborSideNode = this.scanLine.NextHigh(highSideNode);
                if ((nborSideNode.Item is LowObstacleSide) && this.SideReflectsDownward(nborSideNode.Item)) {
                    if (!obstacle.IsOverlapped || !this.ObstacleTree.PointIsInsideAnObstacle(highSide.Start, this.ScanDirection)) {
                        this.StoreLookaheadSite(nborSideNode.Item.Obstacle, highSide, highSide.Start, wantExtreme: true);
                        LoadReflectionEvents(nborSideNode.Item);
                    }
                }
            } 
        }

        void EnqueueHighBendOrCloseVertexEvent(BasicObstacleSide highSide) {
            // If the next side segment after highSide is ascending from the scanline we want to queue another 
            // HighBendVertexEvent; otherwise it is flat or turns down toward the scanline so queue a CloseVertexEvent.
            Obstacle obstacle = highSide.Obstacle;
            PolylinePoint nextHighSideEnd = ScanDirection.IsHorizontal  // Traverse clockwise or counterclockwise
                                        ? highSide.EndVertex.PrevOnPolyline 
                                        : highSide.EndVertex.NextOnPolyline;
            if (ScanDirection.ComparePerpCoord(nextHighSideEnd.Point, highSide.End) > 0) {
                eventQueue.Enqueue(new HighBendVertexEvent(obstacle, highSide.EndVertex));
            }
            else {
                eventQueue.Enqueue(new CloseVertexEvent(obstacle, highSide.EndVertex));
            }
        } // end ProcessEvent(HighBendVertexEvent)

        void CreateCloseEventSegmentsAndFindNeighbors(CloseVertexEvent closeVertEvent) {
            var obstacle = closeVertEvent.Obstacle;
            DevTraceIfFlatSide(false /*isObstacleOpen*/, obstacle.ActiveLowSide.End, obstacle.ActiveHighSide.End);
            RBNode<BasicObstacleSide> lowSideNode = scanLine.Find(obstacle.ActiveLowSide);
            RBNode<BasicObstacleSide> highSideNode = scanLine.Find(obstacle.ActiveHighSide);

            // Two sides coming together at a top point will be reverse-ordered in the scanline,
            // because their projections ahead of the intersection are slope-based.  This must
            // be the case in order to maintain scanline consistency.  Therefore we need to
            // check the comparison and reverse the local variables if necessary.  Fortunately
            // we only concern ourselves with the actual Low-vs-High side type for neighbors,
            // not the sides of the obstacle.
            if (1 == scanLine.Compare(obstacle.ActiveLowSide, obstacle.ActiveHighSide)) {
                var temp = lowSideNode;
                lowSideNode = highSideNode;
                highSideNode = temp;
            }

            // As with OpenVertexEvent, the idea here is to find the neighbors and draw the line between them 
            // that includes the event vertex (and any flat top obstacle side), with consideration for overlaps.
            this.FindNeighborsAndProcessVertexEvent(lowSideNode, highSideNode, closeVertEvent);

            // Inner overlaps: any overlapped sides coming out of the obstacle must have reflection events
            // drained for any that were generated from sides of the closing obstacle if they extend
            // outside the closing obstacle.
            if (this.wantReflections && obstacle.IsOverlapped) {
                for (RBNode<BasicObstacleSide> nextNode = scanLine.NextHigh(lowSideNode);
                        nextNode.Item != highSideNode.Item; nextNode = scanLine.NextHigh(nextNode)) {
                    LoadReflectionEvents(nextNode.Item);
                }
            }

            // Remove the obstacle from the Scanline.  This comes after the foregoing which is why it's a
            // separate function; the RBTree modifies RBNodes, so doing the .Remove at the end of a separate 
            // function ensures we won't access RBNodes that may no longer contain what we expect.
            scanLine.Remove(obstacle.ActiveLowSide, closeVertEvent.Site);
            scanLine.Remove(obstacle.ActiveHighSide, closeVertEvent.Site);
        }

        void ProcessEvent(CloseVertexEvent closeVertEvent) {
            // This event closes the obstacle.  It removes the ActiveLowSide and ActiveHighSide from the scanline and does
            // not add new sides.  As above, see comments in OpenVertexEvent and its callees for more detailed explanations.
            CreateCloseEventSegmentsAndFindNeighbors(closeVertEvent);
            
            // For reflection, in addition to the delayed lookahead we do in the other *VertexEvents, we need to
            // detect pending reflections up to an already loaded obstacle side.  This may be transitive; using
            // H directions as an example:
            //  (A). Obstacles A and B lean rightward such that a vertical line can be drawn from the lower side
            //    of ObstacleA downward to the right of ObstacleB (missing it) and hitting Obstacle C.
            //  (B). ObstacleC's start point is above the start points of Obstacles A and B.
            //  (C). ObjectC reflects a Lookahead scan up along the line cited in (A).
            // To handle this, whenever we close an object, see if either nbor side spans lookahead scan points.
            // If so, then add the events.  Otherwise, we know that a new side for those nbor objects, or for
            // subsequent higher objects, will be created at some point (unless we run out of obstacles).
            // Since we do reflections only in staircase situations, these lookahead events will be discarded
            // (because the existing edge would have to have already been processed as an immediate neighbor,
            // in order to satisfy the definition of a staircase).  See RectilinearTests.ReflectionsDetectedByAlreadyLoadedSide.

            // Check the neighbors in both directions for reflection events.
            // When querying for lookahead sites for a nborSide, always test the full nborSide range if
            // the opposite nborSide reflects upward, because we will have generated a lookahead site for the
            // current eventObstacle on that upward-reflecting nborSide.  For example, restricting the 
            // lowNborSide lookahead-site query to the currentEventObstacle.ActiveLowSide would pick up
            // any lookahead sites stored on eventObstacle.ActiveLowSide, but would not pick up a lookahead
            // site that the current CloseVertexEvent just stored on the highNborSide).
            // Fix: Updated this to remove restricted-range as it skips the range that includes the far side of the 
            // current obstacle when called for neighbors that extend across the top vertex.
            var lowNborSide = (HighObstacleSide)LowNeighborSides.LowNeighbor.Item;
            var highNborSide = (LowObstacleSide)HighNeighborSides.HighNeighbor.Item;
            var obstacle = closeVertEvent.Obstacle;
            LoadReflectionEvents(lowNborSide);
            LoadReflectionEvents(highNborSide);

            // This prepares the object for the second (perpendicular) sweep.
            obstacle.Close();
        } // end ProcessEvent(CloseVertexEvent)

        void ProcessEvent(LowReflectionEvent lowIntEvent) {
            // Unlike LowBendVertexEvent we don't update the ActiveLowSide in the obstacle and scanline.
            var obstacle = lowIntEvent.Side.Obstacle;

            // Add a perpendicular segment from the previous site to the lowNbor intersection, then from
            // the lowNbor intersection to the event site.
            var lowNborSide = scanLine.NextLow(lowIntEvent.Side).Item as HighObstacleSide;
            if (AddPerpendicularReflectionSegment(lowIntEvent, lowIntEvent.Side, lowNborSide)) {
                if (AddParallelReflectionSegment(obstacle, lowNborSide, null /*highNborSide*/, lowIntEvent)) {
                    // We may have just added a reflection that reflects back onto obstacle.ActiveLowSide.
                    LoadReflectionEvents(obstacle.ActiveLowSide);
                }
            }
        } // end ProcessEvent(LowReflectionEvent)

        void ProcessEvent(HighReflectionEvent highIntEvent) {
            // Unlike HighBendVertexEvent we don't update the ActiveHighSide in the obstacle and scanline.
            var obstacle = highIntEvent.Side.Obstacle;

            // Add a perpendicular segment from the previous site to the highNbor intersection, then from
            // the highNbor intersection to the event site.
            var highNborSide = scanLine.NextHigh(highIntEvent.Side).Item as LowObstacleSide;
            if (AddPerpendicularReflectionSegment(highIntEvent, highIntEvent.Side, highNborSide)) {
                if (AddParallelReflectionSegment(obstacle, null /*lowNborSide*/, highNborSide, highIntEvent)) {
                    // We may have just added a reflection that reflects back onto obstacle.ActiveHighSide.
                    LoadReflectionEvents(obstacle.ActiveHighSide);
                }
            }
        } // end ProcessEvent(HighReflectionEvent)

        internal Point MakeInBoundsLocation(Point location) {
            double xPos = Math.Max(location.X, ObstacleTree.GraphBox.Left);
            double yPos = Math.Max(location.Y, ObstacleTree.GraphBox.Bottom);
            return new Point(Math.Min(xPos, ObstacleTree.GraphBox.Right), Math.Min(yPos, ObstacleTree.GraphBox.Top));
        }

        internal bool IsInBounds(VisibilityVertex vertex) {
            return IsInBounds(vertex.Point);
        }

        internal bool IsInBounds(Point p) {
            return PointComparer.Equal(p, MakeInBoundsLocation(p));
        }

        #region DevTrace
#if DEVTRACE
        readonly DevTrace visGraphGenTrace = new DevTrace("Rectilinear_VisGraphGenTrace", "VisGraphGen");
        //unused DevTrace VisGraphGenVerify = new DevTrace("Rectilinear_VisGraphGenVerify");
        readonly DevTrace scanSegmentDump = new DevTrace("Rectilinear_ScanSegmentDump");
#endif // DEVTRACE

        [Conditional("DEVTRACE")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        protected void DevTraceInfoVgGen(int verboseLevel, string format, params object[] args) {
#if DEVTRACE
            visGraphGenTrace.WriteLineIf(DevTrace.Level.Info, verboseLevel, format, args);
#endif // DEVTRACE
        }

        [Conditional("DEVTRACE")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        void DevTraceIfFlatSide(bool isObstacleOpen, Point lowEndpoint, Point highEndpoint) {
#if DEVTRACE
            if (lowEndpoint != highEndpoint) {
                visGraphGenTrace.WriteLineIf(DevTrace.Level.Info, 2, "IsFlat side on {0}VertexEvent: [{1}{2}]"
                                , isObstacleOpen ? "Open" : "Close", lowEndpoint, highEndpoint);
            }
#endif // DEVTRACE
        }

        [Conditional("DEVTRACE")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        void DevTraceScanSegmentDump(int verboseLevel) {
#if DEVTRACE
            if (scanSegmentDump.IsLevel(verboseLevel)) {
                StaticGraphUtility.Test_DumpScanSegments(ObstacleTree, HorizontalScanSegments, VerticalScanSegments);
            }
#endif // DEVTRACE
        }
        #endregion // DevTrace
    }
}
